package game;

import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JPanel;

public class GrandeCanvas extends JPanel implements Runnable {
	/*
	 * TODO:
	 *   Add in motion adjustment for scrolling? (AKA if falling fast down, show more bottom, etc)
	 */
	public Mario mario; //Mario!!!
	public static volatile boolean isRunning = false; //boolean to tell if the game is running
	public static volatile boolean gameOver = true; //boolean to tell if the game is gameOver
	public static volatile boolean paused = true; //tells if the game is over or not
	public static final long serialVersionUID = 1L; //serial version
	public static final int DELAYS_PER_YIELD = 10;
	public Thread animator; //thread of this class
	public int fps = 40;  //desired fps
	public Image buffer = null; //buffer for painting to screen
	public Graphics2D bufGraphics; //graphics for the buffer
	public int period = 1000000000/fps;  //time to sleep in order to reach desired fps
	public static final int MAX_FRAME_SKIPS = 5;  //max frame skips
	public Image timFrame; //current frame of the mario sprite
	public boolean rightDown = false;  //if the right arrow key is down
	public boolean leftDown = false;  //if the left arrow key is down
	public int fwidth; //frame preferred width
	public int fheight; //frame preferred height
	public Color t1 = new Color(0, 0, 0, 150); //a black, semi-transparent color for pausing
	public float friction; //how slippery the surfaces are
	public float gravity; //amount of gravity
	public int mouseX; //mouse x coordinate (on click)
	public int mouseY; //mouse y coordinate (on click)
	public boolean drawing = false; //if a rectangle is being drawn
	public Rectangle drawRect = new Rectangle();
	public Point mousePos = MouseInfo.getPointerInfo().getLocation(); //Point of the current mouse position (MouseInfo class)
	public Point relMousePos = new Point();
	public Point gameMousePos = new Point();
	public Point windowOrgin; //orgin of the frame
	public Insets containerInsets; //container insets (AKA the title bar and border if applicable)
	public Container c; //root class of the JFrame
	public Rectangle removing; //rectangle that is (possibly) being removed
	public Rectangle current; //current rectangle being tested for intersection
	public int currentX;  //current index of scrolling for the game
	public int maxWidth;  //maximum width of game;
	public int maxHeight; //maximum height of game;
	public int currentY; //current index of vertical scrolling for the game
	public int heightVal;  //max amount (in pixels) mario can get toward the screen top/bottom (if not at game bounds)
	public int horizVal;  //max amount (in pixels) mario can get toward the left/right edge (if not at game bounds)
	public Dimension screen;  //dimension of the computer screen
	public int width = 1000; //actual width of the game
	public int height = 800; //actual height of the game
	public BufferedImage back;
	public Menu menu;
	public MainMenu main;
	public int moveLeft = KeyEvent.VK_A;
	public int moveRight = KeyEvent.VK_D;
	public int moveJump = KeyEvent.VK_W;
	public ArrayList<MusicPlayer> players;
	public Level level = new Level(this, new File("levels/base.lvl"));
	public Settings settings = new Settings();
	public Rectangle selected = null;
	public Rectangle selected2 = null;
	public int drawPlatType = 1;
	public ClassCollection drawVars;
	public Color drawColor = Color.red;
	public boolean drawOn = false;
	public boolean removeOn = false;
	public boolean selectOn = false;
	public boolean enemyAdd = false;
	public Console con;
	private boolean conOn = true;
	public boolean noClip = false;
	public boolean muted = false;

	public GrandeCanvas(){
		screen = Toolkit.getDefaultToolkit().getScreenSize();
		try{
			back = ImageIO.read(new File("images/back.png"));
		}catch (Exception e){}
		fwidth = screen.width;
		fheight = screen.height;
		setBackground(Color.white);
		setPreferredSize(new Dimension(width, height));
		setFocusable(true); //allows JPanel to recieve actions
		requestFocus();
		readyForKeyPress();  //make sure to add in termination code
		addMouseListener(new MouseAdapter(){ //allow the game to deal with mouse presses
			@Override
			public void mousePressed(MouseEvent e){
				if(!gameOver){
					if(!paused){
						mousePress(e.getX(), e.getY());
					}else{
						menu.mousePress(e);
					}
				}else{
					main.mousePress(e);
				}
			}
			@Override
			public void mouseReleased(MouseEvent e){
				if(!gameOver){
					if(!paused){
						mouseRelease(e.getX(), e.getY());
					}else{
						menu.mouseRelease(e);
					}
				}else{
					main.mousePress(e);
				}
			}
		});
		initializeComponents();
		
		//start game
		isRunning = true;
		paused = true;
		gameOver = true;
	}
	
	//initialize all of the game components
		protected void initializeComponents(){
			Object[] temp = {100, 1, 1f, "images/bricks.gif"};
			drawVars = new ClassCollection(temp);
			//set base physics values
			friction = 1f; //.5 is the norm (for now).  lower is slippier
			gravity = 2f;  //.5 is the norm (for now).  lower is less
			//set maximum x & y for scrolling
			maxWidth = 2500;
			maxHeight = 1600;
			//set scrolling boundaries
			heightVal = fheight/3;
			horizVal = fwidth/3;
			//set current orgin positions for scrolling
			currentX = 0;
			currentY = 0;
			menu = new PauseMenu(this);
			//player.start();
			players = new ArrayList<MusicPlayer>();
			addMusic(new OggPlayer(new File("images/2nd Flight.ogg"), settings.volume));
			main = new MainMenu(this);
			//level.enemies.add(new Enemy(level, 200, 200, "", 6, 1f));
		}
	
	//adds the canvas to the client, then automatically starts it
	@Override
	public void addNotify(){
		super.addNotify(); //JPanel addNotify (creates peer for the canvas)
		startGame();  //begin the game
	}	
	
	private void startGame(){
		//if the game hasn't already started, start it
		if(animator==null || !isRunning){
			animator = new Thread(this);
			animator.start();
		}
	}
	
	/*
	 * PRO TIP:
	 * 
	 * 
	 * Don't mess with this method (run, that is). Just don't
	 * 
	 */
	@Override
	public void run(){
		//main animation method
		long before, after, timeDiff, sleepTime;  //used in maintaining desired fps/ups
		long overSleepTime = 0L; //amount of time machine overslept
		int delays = 0; //counts number of delays(when machine didn't sleep)
		long extraTime = 0L; //how much time the machine overused for updating/rendering
		int skips = 0; //current number of skips
		sleepTime = 1000L/fps;
		c = getTopLevelAncestor();
		//do top level calculations for drawing
		containerInsets = c.getInsets();
		windowOrgin = new Point(containerInsets.left+c.getX(), containerInsets.top+c.getY());
		
		while(isRunning){  //might need to change how all of this works, allowing animation to continue while gameOver
			//update/render/paint the game
			before = System.nanoTime();
			calculateMousePos();
			if(!gameOver){
				if(!paused){
					try{
						updateGame();
						renderGame();
					}catch (ConcurrentModificationException e){}
				}else{
					renderGame();
					menu.updateMenu();
					menu.draw();
				}
			}else{
				main.updateMenu();
				main.draw();
			}
			paintGame();
			after = System.nanoTime();
			timeDiff = after-before;
			sleepTime = (period - timeDiff) - overSleepTime; //calculate desired time to sleep
			if(sleepTime>0){
				try{
					Thread.sleep(sleepTime/1000000L);  //sleep
				}catch (InterruptedException e){}
				overSleepTime = (System.nanoTime()-after)-sleepTime; //calculate how long the machine overslept
			}else{
				extraTime -= sleepTime;
				overSleepTime = 0L;
				System.out.println("x");
				//allow other essential threads to execute
				if(++delays >= DELAYS_PER_YIELD){
					Thread.yield();
					delays = 0;
				}
			}
			//update without rendering if the whole process takes too long
			while((extraTime>=period) && (skips<=MAX_FRAME_SKIPS)){
				if(!paused && !gameOver){
					updateGame();
				}
				extraTime -= period;
				skips++;
			}
			skips = 0;
		}
		settings.save();
		for(MusicPlayer p: players){
			p.finish();
		}
		System.exit(0);
	}

	//Updates game/character state.  Doesn't, however, write the update to a buffer
	protected void updateGame(){
		//rounds the gravity and friction to one decimal place
		friction = ((float)Math.round(100*friction))/100;
		gravity = ((float)Math.round(100*gravity))/100;
		level.mario.update();
		for(ParticleAnimation p: level.animations){
			p.update();
		}
		for(Enemy e: level.enemies){
			e.update();
		}
		for(Platform i: level.platforms){
			i.update();
		}
		for(Field i: level.fields){
			i.update();
		}
		updateWindow();
		calculateDrawing();
		calculateScrolling();
		fieldIntersectionTest();
		platformIntersectionTest();
	}
	
	protected void platformIntersectionTest(){
		for(Platform i: level.platforms){
			if(level.mario.moveBounds.intersects(i) && !noClip){
				i.calculateIntersectEffects(level.mario);
			}
			for(Enemy e: level.enemies){
				if(e.moveBounds.intersects(i)){
					i.calculateIntersectEffects(e);
				}
			}
		}
	}
	
	protected void fieldIntersectionTest(){
		level.mario.resetEffects();
		for(Enemy e: level.enemies){
			e.resetEffects();
		}
		for(Field i: level.fields){
			if(level.mario.moveBounds.intersects(i) && !noClip){
				i.calculateIntersectEffects(level.mario);
			}
		}
	}
	
	//re-adjusts game based on screen/frame insets and frame position
	protected void adjustForFramePosition(){
		containerInsets = getTopLevelAncestor().getInsets();
		windowOrgin.setLocation(containerInsets.left+c.getX(), containerInsets.top+c.getY());
	}
	
	//deals with the coordinate scrolling
		protected void calculateScrolling(){
			//calculate y scrolling
			if(level.mario.y < currentY+heightVal){
				currentY = level.mario.y-heightVal;
			}else if(level.mario.y > currentY+height-heightVal){
				currentY = level.mario.y+heightVal-height;
			}
			if(currentY < -maxHeight+height){
				currentY = -maxHeight+height;
			}else if(currentY > 0){
				currentY = 0;
			}
			//calculate x scrolling
			if(currentX+width-horizVal < level.mario.x){
				currentX = level.mario.x+horizVal-width;
			}else if(currentX+horizVal > level.mario.x){
				currentX = level.mario.x-horizVal;
			}
			if(currentX < 0){
				currentX = 0;
			}else if(currentX > maxWidth-width){
				currentX = maxWidth - width;
			}
		}
		
		protected void calculateMousePos(){
			mousePos.setLocation(MouseInfo.getPointerInfo().getLocation());
			relMousePos.setLocation(mousePos.x-c.getX()-containerInsets.left, mousePos.y-c.getY()-containerInsets.top);
			gameMousePos.setLocation(relMousePos.x+currentX, relMousePos.y+currentY);
		}
		
		//creates coordinates for rectangle drawing
		protected void calculateDrawing(){
			if(mouseX < gameMousePos.x){
				drawRect.x = (mouseX/10) * 10;
				drawRect.width = (gameMousePos.x/10 - mouseX/10) * 10;
			}else{
				drawRect.x = (gameMousePos.x/10) * 10;
				drawRect.width = (mouseX/10 - gameMousePos.x/10) * 10;
			}
			if(mouseY < gameMousePos.y){
				drawRect.y = (mouseY/10) * 10;
				drawRect.height = (gameMousePos.y/10 - mouseY/10) * 10;
			}else{
				drawRect.y = (gameMousePos.y/10) * 10;
				drawRect.height = (mouseY/10 - gameMousePos.y/10) * 10;
			}
		}
	
	//Renders updated game state to a buffer
	protected void renderGame(){
		if(buffer == null){
			buffer = createImage(fwidth, fheight);
		}else{
			bufGraphics = (Graphics2D)buffer.getGraphics();
			bufGraphics.setColor(Color.white); //clear background
			bufGraphics.fillRect(0, 0, width, height);
			level.draw();
			if(conOn)
			bufGraphics.setColor(Color.black);
			if(drawing){
				bufGraphics.drawRect(drawRect.x-currentX, drawRect.y-currentY, drawRect.width, drawRect.height);
			}
			if(level.disProp){
			bufGraphics.drawString("MousePos X: "+gameMousePos.x, 20, 170);
			bufGraphics.drawString("MousePos Y:  "+gameMousePos.y, 20, 190);
			bufGraphics.setColor(t1);
			}
			if(selected != null){
				bufGraphics.fillRect(selected.x-currentX, selected.y-currentY, selected.width, selected.height);
			}
			if(selected2 != null){
				bufGraphics.fillRect(selected2.x-currentX, selected2.y-currentY, selected2.width, selected2.height);
			}
		}
	}
	
	protected void paintGame(){
		//paints buffer to screen
		Graphics g;
		try{
			g = this.getGraphics();
			//if both can be painted
			if((g != null) && (buffer != null)){
				g.drawImage(buffer, 0, 0, null);
			}else{}
			g.dispose();
		}catch (Exception e){
			System.out.println("Graphics error: "+e);
		}
	}
	
	protected void readyForKeyPress(){
		addKeyListener(new KeyAdapter(){//use keyboard to allow for exit
			@Override
			public void keyPressed(KeyEvent e){
				if(!gameOver){
					if(!paused){
						keyPress(e.getKeyCode());
					}else{
						menu.keyPress(e.getKeyCode());
					}
				}else{
					main.keyPress(e.getKeyCode());
				}
			}
			@Override
			public void keyReleased(KeyEvent e){
				if(!paused){
					keyRelease(e.getKeyCode());
				}
			}
		});
		
	}
	
	//deal with the mouse press coordinates
	protected void mousePress(int x, int y){
		if(drawOn){
			drawing = true;
			mouseX = x+currentX;
			mouseY = y+currentY;
			if(drawing){
				switch(drawPlatType){
					case 13: level.enemies.add(new Enemy(level, mouseX, mouseY, "", 6, .75f)); drawing = false; break;
					case 15: level.ChangeSpawn(mouseX, mouseY);drawing = false; break;
				}
			}
		}else if(removeOn){
			for(Rectangle i: level.platforms){
				if(i.contains(x+currentX, y+currentY)){
					level.platforms.remove(i);
					return;
				}
			}
			for(Rectangle i: level.fields){
				if(i.contains(x+currentX, y+currentY)){
					level.fields.remove(i);
					drawing = false;
					return;
				}
			}
			for(Rectangle i: level.enemies){
				if(i.contains(x+currentX, y+currentY)){
					level.enemies.remove(i);
					drawing = false;
					return;
				}
			}
		}else if(selectOn){
			Rectangle current = getCurrentRect(x, y);
			if(current.equals(selected)){
				selected = null;
				return;
			}else if(current.equals(selected2)){
				selected2 = null;
				return;
			}
			if(selected == null){
				selectFirstRect(x, y);
			}else{
				selectSecondRect(x, y);
			}
		}
	}
	
	public Rectangle getCurrentRect(int x, int y){
		for(Rectangle i: level.platforms){
			if(i.contains(x+currentX, y+currentY)){
				return i;
			}
		}
		for(Rectangle i: level.fields){
			if(i.contains(x+currentX, y+currentY)){
				return i;
			}
		}
		return null;
	}
	public void selectFirstRect(int x, int y){
		for(Rectangle i: level.platforms){
			if(i.contains(x+currentX, y+currentY)){
				selected = i;
				return;
			}
		}
		for(Rectangle i: level.fields){
			if(i.contains(x+currentX, y+currentY)){
				selected = i;
				return;
			}
		}
		selected = null;
	}
	
	public void selectSecondRect(int x, int y){
		for(Rectangle i: level.platforms){
			if(i.contains(x+currentX, y+currentY)){
				selected2 = i;
				return;
			}
		}
		for(Rectangle i: level.fields){
			if(i.contains(x+currentX, y+currentY)){
				selected2 = i;
				return;
			}
		}
		selected2 = null;
	}
	
	protected void mouseRelease(int x, int y){
		if(drawOn){
			if(drawing){
				if(drawRect.width > 0 && drawRect.height > 0){
					GameRectangle g = new Platform(level, 0, 0, 0, 0, drawColor);
					switch(drawPlatType){
					case 1:	g = new Platform(level, drawRect.x, drawRect.y, drawRect.width, drawRect.height, drawColor);break;
					case 2:	g = new MovingPlatform(level, drawRect.x, drawRect.y, drawRect.width, drawRect.height, drawColor, drawVars.get(0, int.class), drawVars.get(1, int.class));break;
					case 3:	g = new Elevator(level, drawRect.x, drawRect.y, drawRect.width, drawRect.height, drawColor, drawVars.get(0, int.class), drawVars.get(1, int.class));break;
					case 4:	g = new FrictionField(level, drawRect.x, drawRect.y, drawRect.width, drawRect.height, new Color(drawColor.getRed(), drawColor.getGreen(), drawColor.getBlue(), 100), drawVars.get(2, float.class));break;
					case 5:	g = new GravityField(level, drawRect.x, drawRect.y, drawRect.width, drawRect.height, new Color(drawColor.getRed(), drawColor.getGreen(), drawColor.getBlue(), 100), drawVars.get(2, float.class));break;
					case 6:	g = new SkinnedBlock(level, drawRect.x, drawRect.y, drawRect.width, drawRect.height, "images/bricks.gif");break;
					case 7:	g = new SpeedField(level, drawRect.x, drawRect.y, drawRect.width, drawRect.height, new Color(drawColor.getRed(), drawColor.getGreen(), drawColor.getBlue(), 100), drawVars.get(0, int.class));break;
					case 8:	g = new Booster(level, drawRect.x, drawRect.y, drawRect.width, drawRect.height, new Color(drawColor.getRed(), drawColor.getGreen(), drawColor.getBlue(), 100), drawVars.get(0, int.class));break;
					case 9: g = new Door(level, drawRect.x, drawRect.y, drawRect.width, drawRect.height, drawColor, drawVars.get(0, int.class), drawVars.get(1, int.class));break;
					case 10: g = new Button(level, drawRect.x, drawRect.y, drawRect.width, drawRect.height);break;
					case 11: g = new Teleporter(level, drawRect.x, drawRect.y, drawRect.width, drawRect.height, new Color(drawColor.getRed(), drawColor.getGreen(), drawColor.getBlue(), 100));break;
					case 12: g = new Spike(level,drawRect.x, drawRect.y, drawRect.width, drawRect.height);break;
					case 14: g = new SkinnedBlock(level, drawRect.x, drawRect.y, drawRect.width, drawRect.height, "images/question_block.gif");break;
					}
					level.add(g);
				}
				drawing = false;
			}
		}
	}
	
	//deal with key press codes (make sure to add in termination code)
	private void keyPress(int keycode){
		if(keycode == moveLeft){
			leftDown = true;
			if(level.mario.getAnim() != 3 && level.mario.getAnim() != 2){
				level.mario.setAnim(1);
			}
		}else if(keycode == moveRight){
			rightDown = true;
			if(level.mario.getAnim() != 2 && level.mario.getAnim() != 3){
				level.mario.setAnim(0);
			}
		}else if(keycode == KeyEvent.VK_ESCAPE){
			drawing = false;
			paused = true;
		}else if(keycode == moveJump){
			level.mario.jumping = true;
		}else if(keycode == KeyEvent.VK_Q){
			drawing = false;
		}else if(keycode == KeyEvent.VK_Y){
			level.mario.speed++;
		}else if(keycode == KeyEvent.VK_H){
			level.mario.speed--;
		}else if(keycode == KeyEvent.VK_U){
			level.mario.jumpHeight++;
		}else if(keycode == KeyEvent.VK_J){
			level.mario.jumpHeight--;
		}else if(keycode == KeyEvent.VK_I){
			gravity += .1;
		}else if(keycode == KeyEvent.VK_K){
			gravity -= .1;
		}else if(keycode == KeyEvent.VK_O){
			friction += .1;
		}else if(keycode == KeyEvent.VK_L){
			friction -= .1;
		}else if(keycode == KeyEvent.VK_CONTROL){
			try{
				con.comeInView();
			}catch(NullPointerException e){
				con = new Console(level);
				con.create();
			}
		}
		
	}
	
	private void keyRelease(int keycode){
		if(keycode == moveLeft){
			leftDown = false;
			if(!rightDown){
				level.mario.setAnim(4);
			}else{
				level.mario.setAnim(0);
			}
		}else if(keycode == moveRight){
			rightDown = false;
			if(!leftDown){
				level.mario.setAnim(5);
			}else{
				level.mario.setAnim(1);
			}
		}else if(keycode == moveJump){
			level.mario.jumping = false;
		}
	}
	
	public synchronized void pauseGame(){
		paused = true;
	}
	
	public synchronized void resumeGame(){
		paused = false;
	}
	
	public synchronized void endGame(){
		gameOver = true;
	}
	
	public synchronized void restartGame(){
		gameOver = false;
	}
	
	public synchronized void quitGame(){
		gameOver = true;
		paused = true;
		isRunning = false;
	}
	
	public void stopMusic(MusicPlayer p){
		p.finish();
		players.remove(p);
	}
	
	public void addMusic(MusicPlayer p){
		players.add(p);
		players.get(players.size()-1).start();
	}
	
	public void updateWindow(){
		fwidth = getWidth() - (containerInsets.left + containerInsets.right);
		fheight = getHeight() - (containerInsets.top + containerInsets.bottom);
		width = getWidth();
		height = getHeight();
	}
}
